﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using Microsoft.SharePoint;

namespace Examples.CAMLFilter
{

    /// <summary>
    /// Источник значения для фильтра
    /// </summary>
    public enum ValueSource : int
    {
        /// <summary>
        /// Значение явно хранится в поле Value 
        /// </summary>
        Constant = 1,

        /// <summary>
        /// В Value - ключ из коллекции HttpRequest.Params
        /// </summary>
        Request = 2,

        /// <summary>
        /// В Value - ключ из коллекции HttpRequest.QueryString
        /// </summary>
        URL = 3,

        /// <summary>
        /// В Value - ключ из коллекции HttpRequest.Form
        /// </summary>
        Form = 4,

        /// <summary>
        /// В Value - ключ из коллекцииHttpRequestHttpRequest.Cookies
        /// </summary>
        Cookie = 5
    }

    /// <summary>
    /// Условие для CAML-запроса
    /// </summary>
    public enum Condition : int
    {
        // Стандартные условия
        Eq = 1,
        Neq = 2,
        Gt = 3,
        Geq = 4,
        Lt = 5,
        Leq = 6,
        IsNull = 7,
        IsNotNull = 8,
        BeginsWith = 9,
        Contains = 10,
        In = 11,
        Includes = 12,
        NotIncludes = 13,

        // Самодельные условия
        AllIncludes = 112,
        AllNotIncludes = 113,

        /// <summary>
        /// Проверка, попадает ли значение в диапазон (value Geq minVal and value Leq maxVal). minVal и maxVal передаются парами параметров
        /// </summary>
        Between = 120,

        /// <summary>
        /// Проверка, не попадает ли значение в диапазон (value Lt minVal and value Gt maxVal). minVal и maxVal передаются парами параметров
        /// </summary>
        NotBetween = 121,

        /// <summary>
        /// Cтрочка с CAML-запросом. Параметры: {0} - FieldName, {1} - FieldType, {2} и т.д. - FilterValue[]
        /// </summary>
        CAML = 200

    }

    /// <summary>
    /// Addditional supported custom field types
    /// </summary>
    public class CustomFieldTypes
    {

        public const String MyLookup = "MyLookup";

        /// <summary>
        /// Справочник соответствия кастомных типов и стандартных (для CAML-запроса)
        /// </summary>
        public static Dictionary<String, String> SupportedTypes = new Dictionary<String, String> { 
            { MyLookup, SPFieldType.Lookup.ToString() }
        };

    }

    /// <summary>
    /// Определение фильтра
    /// </summary>
    [Serializable]
    public class Filter
    {

        /// <summary>
        /// Разделитель по умолчанию для нескольких значений
        /// </summary>
        public const String DefaultDelimiter = ";";

        /// <summary>
        /// Internal Name поля, по которому фильтровать
        /// </summary>
        public String FieldName { get; private set; }

        /// <summary>
        /// Тип поля
        /// </summary>
        public String FieldType { get; private set; }

        /// <summary>
        /// Условие фильтра
        /// </summary>
        public Condition Condition { get; private set; }

        /// <summary>
        /// Введенное пользователем значение для фильтра
        /// </summary>
        public String Value { get; private set; }

        /// <summary>
        /// Источник значения (явно задано или берется через имя параметра)
        /// </summary>
        public ValueSource Source { get; private set; }

        /// <summary>
        /// Передается несколько значений фильтра
        /// </summary>
        public Boolean IsMulti { get; private set; }

        /// <summary>
        /// Разделитель значений
        /// </summary>
        public String Delimiter { get; private set; }

        /// <summary>
        /// Допустимы ли пустые значения
        /// </summary>
        public Boolean AllowEmptyValue { get; private set; }


        /// <summary>
        /// Искать по LookupId вместо значения
        /// </summary>
        public Boolean UseLookupId { get; private set; }

        /// <summary>
        /// Для полей дат включать время
        /// </summary>
        public Boolean IncludeTimeValue { get; private set; }


        /// <summary>
        /// Введенный пользователем CAML-запрос
        /// </summary>
        public String CAML { get; private set; }

        /// <summary>
        /// Фильтр включен
        /// </summary>
        public Boolean IsActive { get; private set; }


        /// <summary>
        /// Конструктор, который приводит в порядок переданные параметры
        /// </summary>
        /// <param name="fieldName">Имя поля</param>
        /// <param name="fieldType">Тип поля</param>
        /// <param name="condition">Условие сравнения</param>
        /// <param name="value">Значение фильтра</param>
        /// <param name="source">Как передается значение фильтра</param>
        /// <param name="isMulti">Разрешена передача нескольких значений</param>
        /// <param name="delimiter">Разделитель значений</param>
        /// <param name="allowEmptyValue">Разрешено передавать пустые значения</param>
        /// <param name="isActive">Фильтр активен</param>
        /// <param name="caml">Введенное пользователем CAML-выражение</param>
        public Filter(String fieldName, String fieldType, Condition condition, String value, ValueSource source, Boolean isMulti, String delimiter, Boolean allowEmptyValue, Boolean useLookupId, Boolean includeTimeValue, Boolean isActive, String caml)
        {
            this.FieldName = fieldName;
            this.FieldType = fieldType;
            this.Condition = condition;
            this.Source = source;
            this.IsActive = isActive;

            switch (this.Condition)
            {
                // Для проверки на пустоту само значение не нужно - только факт передачи в параметрах
                case Condition.IsNull:
                case Condition.IsNotNull:
                    this.Value = (this.Source == ValueSource.Constant) ? String.Empty : value;
                    this.AllowEmptyValue = true;
                    this.IsMulti = false;
                    this.Delimiter = null;
                    this.UseLookupId = false;
                    this.CAML = null;
                    break;

                // Для условий сравнения множественные значения не имеют смысла
                case Condition.Geq:
                case Condition.Gt:
                case Condition.Leq:
                case Condition.Lt:
                    this.Value = value;
                    this.AllowEmptyValue = allowEmptyValue;
                    this.IsMulti = false;
                    this.Delimiter = null;
                    this.UseLookupId = useLookupId;
                    this.CAML = null;
                    break;

                // Условия диапазона и множества всегда со множественными непустыми значениями
                case Condition.Between:
                case Condition.NotBetween:
                case Condition.In:
                case Condition.AllIncludes:
                case Condition.AllNotIncludes:
                    this.Value = value;
                    this.AllowEmptyValue = false;
                    this.IsMulti = true;
                    this.Delimiter = String.IsNullOrEmpty(delimiter) ? Filter.DefaultDelimiter : delimiter;
                    this.UseLookupId = useLookupId;
                    this.CAML = null;
                    break;

                case Condition.CAML:
                    this.Value = value;
                    this.AllowEmptyValue = allowEmptyValue;
                    this.IsMulti = isMulti;
                    this.Delimiter = delimiter;
                    this.UseLookupId = false;
                    this.CAML = caml;
                    break;

                // Дефолтный вариант - сохраняем все параметры кроме CAML
                default:
                    this.Value = value;
                    this.AllowEmptyValue = allowEmptyValue;
                    this.IsMulti = isMulti;
                    this.Delimiter = delimiter;
                    this.UseLookupId = useLookupId;
                    this.CAML = null;
                    break;

            }


            this.IncludeTimeValue = this.FieldType.Equals(SPFieldType.DateTime.ToString()) ? includeTimeValue : false;

            this.Delimiter = this.IsMulti ? (String.IsNullOrEmpty(delimiter) ? Filter.DefaultDelimiter : delimiter) : null;
        }


        // Шаманство следующее. В Value может сидеть как прямое значение (для ValueSources.Constant), так и 

        // ключи для различных коллекций. Оно заполняется на этапе редактирования веб-части.
        // Чтобы получить реальное значение для фильтра, есть свойство только для чтения
        // Но чтобы получить его, надо предварительно вызвать ResolveValue()

        /// <summary>
        /// Реальные значения для фильтра
        /// </summary>
        public string[] FilterValue
        {
            get
            {
                if (_Resolved)
                    return _resolvedValue;
                else
                    throw new Exception("ResolveValue should be called before using FilterValue");
            }
        }

        // Но чтобы оно корректно работало для некоторых фильтров, надо предварительно вызвать ResolveFilterValue()

        /// <summary>
        /// Возвращает значение в зависимости от типа ValueSource и (опционально) параметров
        /// </summary>
        /// <param name="data">Объекты для извлечения параметров</param>
        public void ResolveFilterValue(HttpRequest request, Boolean? isPostBack, StateBag viewState)
        {
            if (!_Resolved)
            {
                string str = null;
                switch (this.Source)
                {

                    // Явно заданное значение
                    case ValueSource.Constant:
                        str = Value;
                        break;


                    // Значение из запроса
                    case ValueSource.Request:
                    case ValueSource.URL:
                    case ValueSource.Form:
                        // Здесь предполагаем, что нам переданы параметры HttpRequest, bool IsPostBack и ViewState 
                        if (request == null || isPostBack == null || viewState == null)
                            throw new Exception(String.Format("Invalid call ResolveFilterValue for value source '{0}'", this.Source.ToString()));

                        // Берем ключ для хранилища ViewState

                        String vsKey = this.Key;

                        // Если оригинальный заход на страницу - берем данные из запроса
                        // и сохраняем их во ViewState
                        if (!(Boolean)isPostBack)
                        {
                            viewState.Remove(vsKey);

                            // Берем значение из нужной коллекции запроса
                            str = (this.Source == ValueSource.Request ? HttpContext.Current.Request.Params[Value] :
                                        (this.Source == ValueSource.URL ? HttpContext.Current.Request.QueryString[Value] :
                                            HttpContext.Current.Request.Form[Value]));

                            if (str != null)
                                viewState[vsKey] = str;
                        }
                        // Иначе - берем данные из ViewState
                        else
                        {
                            str = viewState[vsKey] as String;
                        }
                        break;

                    // Значение из куки.
                    case ValueSource.Cookie:
                        // Здесь предполагаем, что нам передан параметр HttpRequest
                        if (request == null)
                            throw new Exception(String.Format("Invalid call ResolveFilterValue for value source '{0}'", this.Source.ToString()));

                        if (HttpContext.Current.Request.Cookies[Value] != null) str = HttpContext.Current.Request.Cookies[Value].Value;
                        break;


                    // Заглушка для неопределенных источников
                    default:
                        throw new Exception(String.Format("Not supported value source '{0}'", this.Source.ToString()));
                }

                // Здесь определяем, множественные параметры или нет, а заодно и пустые ли строки прилетели
                if (str != null)
                {
                    if (this.IsMulti)
                    {
                        _resolvedValue = str.Split(new string[] { this.Delimiter }, this.AllowEmptyValue ? StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries);
                        if (_resolvedValue.Length == 0 && !this.AllowEmptyValue) _resolvedValue = null;

                    }
                    else
                        if (!String.IsNullOrEmpty(str) || this.AllowEmptyValue) _resolvedValue = new string[] { str };
                }
                _Resolved = true;
            }
        }

        [NonSerializedAttribute]
        private Boolean _Resolved = false;        // флаг, вычисляли или нет
        [NonSerializedAttribute]
        private string[] _resolvedValue = null;     // Вычисленное значение   



        /// <summary>
        /// Генерирует CAML-запрос для фильтра
        /// </summary>
        /// <returns>CAML-запрос</returns>
        public String Query()
        {

            if (this.FilterValue == null) return null;

            // Приведем тип значения к стандартным
            String valueType = CustomFieldTypes.SupportedTypes.ContainsKey(this.FieldType) ? CustomFieldTypes.SupportedTypes[this.FieldType] : this.FieldType;

            string fieldRef = String.Format("<FieldRef Name='{0}'{1}/>", this.FieldName, this.UseLookupId ? " LookupId='TRUE' " : "");
            string value = String.Format("<Value Type='{0}'{1}>", valueType, this.IncludeTimeValue ? " IncludeTimeValue='TRUE' " : "");

            List<String> parts = new List<string>();
            switch (this.Condition)
            {

                case Condition.Eq:
                    foreach (String val in this.FilterValue)
                    {
                        parts.Add(String.Format("<Eq>{0}{1}{2}</Value></Eq>", fieldRef, value, XmlEncodeValue(val)));
                    }
                    return JoinQueries(parts, "Or");

                case Condition.Neq:
                    foreach (String val in this.FilterValue)
                    {
                        parts.Add(String.Format("<Neq>{0}{1}{2}</Value></Neq>", fieldRef, value, XmlEncodeValue(val)));
                    }
                    return JoinQueries(parts, "And");

                case Condition.Gt:
                    return String.Format("<Gt>{0}{1}{2}</Value></Gt>", fieldRef, value, XmlEncodeValue(this.FilterValue[0]));

                case Condition.Geq:
                    return String.Format("<Geq>{0}{1}{2}</Value></Geq>", fieldRef, value, XmlEncodeValue(this.FilterValue[0]));

                case Condition.Lt:
                    return String.Format("<Lt>{0}{1}{2}</Value></Lt>", fieldRef, value, XmlEncodeValue(this.FilterValue[0]));

                case Condition.Leq:
                    return String.Format("<Leq>{0}{1}{2}</Value></Leq>", fieldRef, value, XmlEncodeValue(this.FilterValue[0]));

                case Condition.IsNull:
                    return String.Format("<IsNull>{0}</IsNull>", fieldRef);

                case Condition.IsNotNull:
                    return String.Format("<IsNotNull>{0}</IsNotNull>", fieldRef);

                case Condition.BeginsWith:
                    foreach (String val in this.FilterValue)
                    {
                        parts.Add(String.Format("<BeginsWith>{0}{1}{2}</Value></BeginsWith>", fieldRef, value, XmlEncodeValue(val)));
                    }
                    return JoinQueries(parts, "Or");


                case Condition.Contains:
                    foreach (String val in this.FilterValue)
                    {
                        parts.Add(String.Format("<Contains>{0}{1}{2}</Value></Contains>", fieldRef, value, XmlEncodeValue(val)));
                    }
                    return JoinQueries(parts, "Or");

                case Condition.In:
                    foreach (String val in this.FilterValue)
                    {
                        parts.Add(String.Format("{0}{1}</Value>", value, XmlEncodeValue(val)));
                    }
                    return String.Format("<In>{0}<Values>{1}</Values></In>", fieldRef, String.Join("", parts.ToArray()));

                case Condition.Includes:
                    foreach (String val in this.FilterValue)
                    {
                        parts.Add(String.Format("<Includes>{0}{1}{2}</Value></Includes>", fieldRef, value, XmlEncodeValue(val)));
                    }
                    return JoinQueries(parts, "Or");

                case Condition.NotIncludes:
                    foreach (String val in this.FilterValue)
                    {
                        parts.Add(String.Format("<NotIncludes>{0}{1}{2}</Value></NotIncludes>", fieldRef, value, XmlEncodeValue(val)));
                    }
                    return JoinQueries(parts, "Or");

                case Condition.AllIncludes:
                    foreach (String val in this.FilterValue)
                    {
                        parts.Add(String.Format("<Includes>{0}{1}{2}</Value></Includes>", fieldRef, value, XmlEncodeValue(val)));
                    }
                    return JoinQueries(parts, "And");

                case Condition.AllNotIncludes:
                    foreach (String val in this.FilterValue)
                    {
                        parts.Add(String.Format("<NotIncludes>{0}{1}{2}</Value></NotIncludes>", fieldRef, value, XmlEncodeValue(val)));
                    }
                    return JoinQueries(parts, "And");

                case Condition.Between:
                    if (this.FilterValue.Length % 2 != 0)
                        throw new Exception(String.Format("Condition '{0}' requires even number of parameters", this.Condition.ToString()));
                    for (int i = 0; i < this.FilterValue.Length; i = i + 2)
                    {
                        parts.Add(String.Format("<And><Geq>{0}{1}{2}</Value></Geq><Leq>{0}{1}{3}</Value></Leq></And>", fieldRef, value, XmlEncodeValue(this.FilterValue[i]), XmlEncodeValue(this.FilterValue[i + 1])));
                    }
                    return JoinQueries(parts, "Or");

                case Condition.NotBetween:
                    if (this.FilterValue.Length % 2 != 0)
                        throw new Exception(String.Format("Condition '{0}' requires even number of parameters", this.Condition.ToString()));
                    for (int i = 0; i < this.FilterValue.Length; i = i + 2)
                    {
                        parts.Add(String.Format("<Or><Lt>{0}{1}{2}</Value></Lt><Gt>{0}{1}{3}</Value></Gt></Or>", fieldRef, value, XmlEncodeValue(this.FilterValue[i]), XmlEncodeValue(this.FilterValue[i + 1])));
                    }
                    return JoinQueries(parts, "And");

                case Condition.CAML:
                    List<String> args = new List<String>() { this.FieldName, valueType };
                    args.AddRange(Array.ConvertAll(this.FilterValue, new Converter<string, string>(XmlEncodeValue)));
                    return String.Format(this.CAML, args.ToArray());


                // Заглушка для неопределенных источников
                default:
                    throw new Exception(String.Format("Not supported condition '{0}'", this.Condition.ToString()));

            }
        }

        /// <summary>
        /// Строка с описанием фильтра для отладки
        /// </summary>
        /// <returns></returns>
        public String Info()
        {
            return String.Format("'{0}' {1} {2} ({3}{4})", this.FieldName, this.Condition.ToString(),
                _Resolved ? (this.FilterValue == null ? "<NULL>" : "'" + String.Join(this.Delimiter, this.FilterValue) + "'") : "<UNRESOLVED>",
                this.Source, this.Source == ValueSource.Constant ? (this.IsMulti ? ", Multi" : "") : (String.Format(":{0}{1}", this.Value, this.IsMulti ? ", Multi" : "")));
        }

        /// <summary>
        /// Заголовок фильтра
        /// </summary>
        /// <returns></returns>
        public String Title
        {
            get
            {
                return String.Format("{0} {1} {2}{3}", this.FieldName, this.Condition, this.Source, (this.Source != ValueSource.Constant ? ":" + this.Value : ""));
            }
        }

        /// <summary>
        /// генерит ключ для сохранения значения в кешах
        /// </summary>
        private String Key
        {
            get
            {
                return String.Format("CAMLFilter_{0}_{1}_{2}_{3}", this.FieldName, this.Value, (int)this.Source, (int)this.Condition);
            }
        }

        // маленький хелпер для кодирования значений
        static private String XmlEncodeValue(String str)
        {
            String res = str;
            res = res.Replace("&", "&amp;");
            res = res.Replace(">", "&gt;");
            res = res.Replace("<", "&lt;");
            res = res.Replace("\"", "&quot;");
            res = res.Replace("'", "&apos;");
            return res;
        }


        static public String JoinQueries(List<String> queries, String condition)
        {
            StringBuilder res = new StringBuilder();
            String tStart = String.Format("<{0}>", condition);
            String tEnd = String.Format("</{0}>", condition);
            foreach (String query in queries)
            {
                if (res.Length == 0)
                {
                    res.Append(query);
                }
                else
                {
                    res.Insert(0, tStart);
                    res.Append(query);
                    res.Append(tEnd);
                }
            }
            return res.ToString();
        }



        /// <summary>
        /// Определяем тип поля
        /// </summary>
        static public String GetFieldType(SPField field)
        {
            if (field.Type != SPFieldType.Invalid || CustomFieldTypes.SupportedTypes.ContainsKey(field.TypeAsString))
                return field.TypeAsString;
            else
            {
                if (!field.FromBaseType)
                    return field.FieldTypeDefinition.BaseRenderingTypeName;
            }

            return null;
        }


    }
}
